<

シマーローディングエフェクトを作成する

アプリケーション開発では読み込み時間は避けられません。 ユーザーエクスペリエンス(UX)の観点から見ると、 最も重要なことはユーザーにそれを示すことです 読み込みが行われているということです。一般的なアプローチの 1 つ データがロードされていることをユーザーに伝えるには、 クロムカラーをきらめくアニメーションで表示します 読み込まれているコンテンツのタイプに近い形状。

次のアニメーションはアプリの動作を示しています。

Gif showing the UI loading

このレシピは、コンテンツ ウィジェットを定義して配置することから始まります。 右下にはフローティング アクション ボタン (FAB) もあります。 ロードモードとロードモードを切り替えるコーナー これにより、実装を簡単に検証できます。

きらめく形を描く

この効果で光る形状は独立しています 最終的に読み込まれる実際のコンテンツから。

したがって、目標は、次のような形状を表示することです。 最終的な内容をできるだけ正確に。

正確な形状を表示するのは簡単です。 コンテンツには明確な境界があります。たとえば、このレシピでは、 いくつかの円形の画像といくつかの角丸長方形の画像があります。 輪郭に正確に一致する形状を描くことができます それらの画像の。

一方、その下に表示されるテキストについて考えてみましょう。 角丸長方形の画像。何行あるかわかりません テキストは読み込まれるまで存在します。 したがって、長方形を描こうとしても意味がありません。 テキストの各行に対して。代わりに、データのロード中に、 非常に薄い角丸長方形をいくつか描きます。 表示されるテキストを表します。形と大きさ 完全には一致しませんが、問題ありません。

画面上部の循環リスト項目から始めます。 それぞれを確認してくださいCircleListItemウィジェットには円が表示されます 画像の読み込み中に色が付きます。

class CircleListItem extends StatelessWidget {
  const CircleListItem({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
      child: Container(
        width: 54,
        height: 54,
        decoration: const BoxDecoration(
          color: Colors.black,
          shape: BoxShape.circle,
        ),
        child: ClipOval(
          child: Image.network(
            'https://flutter'
            '.dev/docs/cookbook/img-files/effects/split-check/Avatar1.jpg',
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

ウィジェットが何らかの形状を表示している限り、 このレシピではシマー効果を適用できます。

に似ていますCircleListItemウィジェット、 確実にCardListItemウィジェット 画像が表示される場所に色を表示します。 また、CardListItemウィジェット、 テキストの表示と 現在の読み込みステータスに基づいて四角形が表示されます。

class CardListItem extends StatelessWidget {
  const CardListItem({
    super.key,
    required this.isLoading,
  });

  final bool isLoading;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _buildImage(),
          const SizedBox(height: 16),
          _buildText(),
        ],
      ),
    );
  }

  Widget _buildImage() {
    return AspectRatio(
      aspectRatio: 16 / 9,
      child: Container(
        width: double.infinity,
        decoration: BoxDecoration(
          color: Colors.black,
          borderRadius: BorderRadius.circular(16),
        ),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(16),
          child: Image.network(
            'https://flutter'
            '.dev/docs/cookbook/img-files/effects/split-check/Food1.jpg',
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }

  Widget _buildText() {
    if (isLoading) {
      return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            width: double.infinity,
            height: 24,
            decoration: BoxDecoration(
              color: Colors.black,
              borderRadius: BorderRadius.circular(16),
            ),
          ),
          const SizedBox(height: 16),
          Container(
            width: 250,
            height: 24,
            decoration: BoxDecoration(
              color: Colors.black,
              borderRadius: BorderRadius.circular(16),
            ),
          ),
        ],
      );
    } else {
      return const Padding(
        padding: EdgeInsets.symmetric(horizontal: 8),
        child: Text(
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do '
          'eiusmod tempor incididunt ut labore et dolore magna aliqua.',
        ),
      );
    }
  }
}

UI は、以下に応じて異なる方法でレンダリングされるようになりました。 ロード中かロード中か。 画像URLを一時的にコメントアウトすることで、 UI が 2 つの方法でレンダリングされることがわかります。

Gif showing the shimmer animation

次の目標は、色付きの領域をすべてペイントすることです。 きらめきのように見える単一のグラデーション。

シマーグラデーションをペイントする

このレシピで達成される効果の鍵は、ウィジェットを使用することです 呼ばれたShaderMask。のShaderMaskウィジェットはその名の通り、 シェーダをその子に適用しますが、その領域にのみ適用します。 子供はすでに何かを描いていました。例えば、 シェーダを適用するのは黒いシェイプのみです。 前に設定しました。

に適用されるクロム色の線形グラデーションを定義します。 きらめく形。

const _shimmerGradient = LinearGradient(
  colors: [
    Color(0xFFEBEBF4),
    Color(0xFFF4F4F4),
    Color(0xFFEBEBF4),
  ],
  stops: [
    0.1,
    0.3,
    0.4,
  ],
  begin: Alignment(-1.0, -0.3),
  end: Alignment(1.0, 0.3),
  tileMode: TileMode.clamp,
);

という新しいステートフル ウィジェットを定義します。ShimmerLoading与えられたものをラップするchildウィジェット付きShaderMask。 を設定します。ShaderMaskシマーを適用するウィジェット シェーダとしてのグラデーションblendModesrcATop。 のsrcATopブレンド モードは、任意の色を置き換えます。childシェーダーカラーでペイントされたウィジェット。

class ShimmerLoading extends StatefulWidget {
  const ShimmerLoading({
    super.key,
    required this.isLoading,
    required this.child,
  });

  final bool isLoading;
  final Widget child;

  @override
  State<ShimmerLoading> createState() => _ShimmerLoadingState();
}

class _ShimmerLoadingState extends State<ShimmerLoading> {
  @override
  Widget build(BuildContext context) {
    if (!widget.isLoading) {
      return widget.child;
    }

    return ShaderMask(
      blendMode: BlendMode.srcATop,
      shaderCallback: (bounds) {
        return _shimmerGradient.createShader(bounds);
      },
      child: widget.child,
    );
  }
}

を包みますCircleListItemウィジェットShimmerLoadingウィジェット。

Widget _buildTopRowItem() {
  return ShimmerLoading(
    isLoading: _isLoading,
    child: const CircleListItem(),
  );
}

を包みますCardListItemウィジェットShimmerLoadingウィジェット。

Widget _buildListItem() {
  return ShimmerLoading(
    isLoading: _isLoading,
    child: CardListItem(
      isLoading: _isLoading,
    ),
  );
}

図形の読み込み中に、表示されるようになりました シマーグラデーション から戻ってきましたshaderCallback

これは正しい方向への大きな一歩です しかし、このグラデーション表示には問題があります。 各CircleListItemウィジェットとそれぞれのCardListItemウィジェット 新しいバージョンのグラデーションを表示します。 このレシピでは、画面全体が 一つの大きなきらめく表面のように見えます。 この問題は次のステップで解決します。

大きなきらめきを 1 つペイントする

画面全体に大きなきらめきをペイントするには、 各ShimmerLoadingウィジェットのニーズ 同じ全画面グラデーションベースのペイント その位置についてShimmerLoading画面上のウィジェット。

より正確に言えば、シマーであると仮定するのではなく、 画面全体を占める必要があります。 輝きを共有する領域がいくつかあるはずです。 おそらくその領域が画面全体を占めるでしょう、 あるいはそうではないかもしれません。これを解決する方法は Flutter における一種の問題は、別のウィジェットを定義することです すべての上に位置するものShimmerLoadingウィジェット ウィジェットツリー内でそれを呼び出しますShimmer。 それでは、それぞれShimmerLoadingウィジェットが参照を取得する にShimmer祖先 そして、表示する希望のサイズとグラデーションを要求します。

という新しいステートフル ウィジェットを定義します。Shimmerそれか を取り込みますLinearGradientそして子孫を提供します にアクセスできるState物体。

class Shimmer extends StatefulWidget {
  static ShimmerState? of(BuildContext context) {
    return context.findAncestorStateOfType<ShimmerState>();
  }

  const Shimmer({
    super.key,
    required this.linearGradient,
    this.child,
  });

  final LinearGradient linearGradient;
  final Widget? child;

  @override
  ShimmerState createState() => ShimmerState();
}

class ShimmerState extends State<Shimmer> {
  @override
  Widget build(BuildContext context) {
    return widget.child ?? const SizedBox();
  }
}

にメソッドを追加します。ShimmerState順番にクラス へのアクセスを提供するため、linearGradient、 のサイズShimmerStateRenderBox、 そして、その中の子孫の位置を調べます。ShimmerStateRenderBox

class ShimmerState extends State<Shimmer> {
  Gradient get gradient => LinearGradient(
        colors: widget.linearGradient.colors,
        stops: widget.linearGradient.stops,
        begin: widget.linearGradient.begin,
        end: widget.linearGradient.end,
      );

  bool get isSized => (context.findRenderObject() as RenderBox?)?.hasSize ?? false;

  Size get size => (context.findRenderObject() as RenderBox).size;

  Offset getDescendantOffset({
    required RenderBox descendant,
    Offset offset = Offset.zero,
  }) {
    final shimmerBox = context.findRenderObject() as RenderBox;
    return descendant.localToGlobal(offset, ancestor: shimmerBox);
  }

  @override
  Widget build(BuildContext context) {
    return widget.child ?? const SizedBox();
  }
}

画面のすべてのコンテンツをShimmerウィジェット。

class _ExampleUiLoadingAnimationState extends State<ExampleUiLoadingAnimation> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Shimmer(
        linearGradient: _shimmerGradient,
        child: ListView(
            // ListView Contents
            ),
      ),
    );
  }
}

使用Shimmerウィジェット内のShimmerLoading共有グラデーションをペイントするウィジェット。

class _ShimmerLoadingState extends State<ShimmerLoading> {
  @override
  Widget build(BuildContext context) {
    if (!widget.isLoading) {
      return widget.child;
    }

    // Collect ancestor shimmer information.
    final shimmer = Shimmer.of(context)!;
    if (!shimmer.isSized) {
      // The ancestor Shimmer widget isn’t laid
      // out yet. Return an empty box.
      return const SizedBox();
    }
    final shimmerSize = shimmer.size;
    final gradient = shimmer.gradient;
    final offsetWithinShimmer = shimmer.getDescendantOffset(
      descendant: context.findRenderObject() as RenderBox,
    );

    return ShaderMask(
      blendMode: BlendMode.srcATop,
      shaderCallback: (bounds) {
        return gradient.createShader(
          Rect.fromLTWH(
            -offsetWithinShimmer.dx,
            -offsetWithinShimmer.dy,
            shimmerSize.width,
            shimmerSize.height,
          ),
        );
      },
      child: widget.child,
    );
  }
}

あなたのShimmerLoadingウィジェットに共有されたものが表示されるようになりました 内のすべてのスペースを占めるグラデーションShimmerウィジェット。

きらめきをアニメーション化する

シマーグラデーションを動かすには、 きらめく輝きを与えます。

LinearGradientというプロパティがありますtransformグラデーションの外観を変えるために使用できます。 たとえば、水平方向に移動します。 のtransformプロパティはGradientTransform実例。

というクラスを定義します_SlidingGradientTransform実装するGradientTransform横方向にスライドする外観を実現します。

class _SlidingGradientTransform extends GradientTransform {
  const _SlidingGradientTransform({
    required this.slidePercent,
  });

  final double slidePercent;

  @override
  Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
    return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);
  }
}

グラデーション スライドのパーセンテージは時間の経過とともに変化します 動きのある外観を作成するため。 パーセンテージを変更するには、AnimationControllerの中にShimmerStateクラス。

class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
  late AnimationController _shimmerController;

  @override
  void initState() {
    super.initState();

    _shimmerController = AnimationController.unbounded(vsync: this)
      ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
  }

  @override
  void dispose() {
    _shimmerController.dispose();
    super.dispose();
  }
}

を適用します。_SlidingGradientTransformgradientを使用して_shimmerControllervalueとしてslidePercent

LinearGradient get gradient => LinearGradient(
      colors: widget.linearGradient.colors,
      stops: widget.linearGradient.stops,
      begin: widget.linearGradient.begin,
      end: widget.linearGradient.end,
      transform:
          _SlidingGradientTransform(slidePercent: _shimmerController.value),
    );

グラデーションがアニメーション化されますが、個人のShimmerLoadingウィジェット自体は再描画されません グラデーションが変化するので。したがって、何もないように見えます 起こっている。

を公​​開します_shimmerControllerからShimmerStateとしてListenable

Listenable get shimmerChanges => _shimmerController;

ShimmerLoading、祖先への変更をリッスンします。ShimmerStateshimmerChanges財産、 そしてシマーグラデーションを再ペイントします。

class _ShimmerLoadingState extends State<ShimmerLoading> {
  Listenable? _shimmerChanges;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (_shimmerChanges != null) {
      _shimmerChanges!.removeListener(_onShimmerChange);
    }
    _shimmerChanges = Shimmer.of(context)?.shimmerChanges;
    if (_shimmerChanges != null) {
      _shimmerChanges!.addListener(_onShimmerChange);
    }
  }

  @override
  void dispose() {
    _shimmerChanges?.removeListener(_onShimmerChange);
    super.dispose();
  }

  void _onShimmerChange() {
    if (widget.isLoading) {
      setState(() {
        // update the shimmer painting.
      });
    }
  }
}

おめでとう! これで全画面表示になりました。 回転するアニメーションのシマーエフェクト コンテンツの読み込みに応じてオンとオフが切り替わります。